<?php
	/*
	 * This file is part of phpitter.
 	 * 
	 * phpitter is free software: you can redistribute it and/or modify
	 * it under the terms of the GNU General Public License as published by
	 * the Free Software Foundation, either version 3 of the License, or
	 * (at your option) any later version.
 	 * 
	 * phpitter is distributed in the hope that it will be useful,
	 * but WITHOUT ANY WARRANTY; without even the implied warranty of
	 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	 * GNU General Public License for more details.
 	 * 
	 * You should have received a copy of the GNU General Public License
	 * along with phpitter.  If not, see <http://www.gnu.org/licenses/>.
	 *
	 * Copyright 2008 Ryan Hadley
	 */

	ini_set('default_socket_timeout', 15);

	class phpTwitter
	{
		const DEBUG = false;

		public $PHP_PATH = ""; // set to "./php-win.exe -q" (or appropriate) for Windows
		var $baseDir;
		var $username;
		var $password;
		var $friendsTimeline = array();
		var $replies = array();
		var $directMessages = array();
		var $directMessagesSent = array();
		var $justSent = array();
		var $friendsTimelineLastPull;
		var $directMessagesLastPull;
		var $status = '';

		var $userAgent = 'phpitter';
		var $maxFriendsUpdatesHistory = 40;
		var $offline = false;

		var $friendsTimelineURL = 'twitter.com/statuses/friends_timeline.xml';
		var $repliesURL = 'twitter.com/statuses/replies.xml';
		var $sendUpdateURL = 'twitter.com/statuses/update.xml';
		var $sendDirectURL = 'twitter.com/direct_messages/new.xml';
		var $directMessagesURL = 'twitter.com/direct_messages.xml';
		var $directMessagesSentURL = 'twitter.com/direct_messages/sent.xml';
		var $showUpdateURL = 'twitter.com/status/show/';
		var $mapDataPostURL = 'phpitter.com/map/ajax/post_tweets.php';

		function phpTwitter($username, $password)
		{
			$this->username = $username;
			$this->password = $password;
			$this->baseDir = dirname(__FILE__).'/';

			if ( is_file($this->baseDir . 'php-win.exe') )
				$this->PHP_PATH = $this->baseDir . 'php-win.exe -q';
		}

		function getAll()
		{
			$allUpdates = $this->friendsTimeline + $this->replies + $this->directMessages + $this->directMessagesSent + $this->justSent;
			usort($allUpdates, array("phpTwitter", "sortByDate"));

			return $allUpdates;
		}

		function saveTweets()
		{
			self::debug("Saving tweets");
			$serializeMe['friendsTimeline'] = $this->friendsTimeline;
			$serializeMe['replies'] = $this->replies;
			$serializeMe['directMessages'] = $this->directMessages;
			$serializeMe['directMessagesSent'] = $this->directMessagesSent;
			$serializeMe['friendsTimelineLastPull'] = $this->friendsTimelineLastPull;
			$serializeMe['directMessagesLastPull'] = $this->directMessagesLastPull;
			$serializeMe['status'] = $this->status;
			file_put_contents($this->username . ".tweets.phpitter", serialize($serializeMe));
		}

		function loadTweets()
		{
			self::debug("Loading tweets");
			if ( is_readable($this->username . ".tweets.phpitter") )
			{
				$tweetData = unserialize(file_get_contents($this->username . ".tweets.phpitter"));
				$this->friendsTimeline = $tweetData['friendsTimeline'];
				$this->replies = $tweetData['replies'];
				$this->directMessages = $tweetData['directMessages'];
				$this->directMessagesSent = $tweetData['directMessagesSent'];
				$this->friendsTimelineLastPull = intval($tweetData['friendsTimelineLastPull']);
				$this->directMessagesLastPull = intval($tweetData['directMessagesLastPull']);
				$this->status = $tweetData['status'];
			}
		}

		function getXMLNonBlocking($type, $callback)
		{
			$data['username'] = $this->username;
			$data['password'] = $this->password;
			$data['type'] = $type;

			$descriptorSpec[0] = array("pipe", "r");
			$descriptorSpec[1] = array("pipe", "w");
			$descriptorSpec[2] = array("pipe", "r");

			self::debug("opening proc to: ".trim($this->PHP_PATH .' '. $this->baseDir . 'phpitter_get_xml.php'));
			$childProc = proc_open(trim($this->PHP_PATH .' '. $this->baseDir . 'phpitter_get_xml.php'), $descriptorSpec, $childPipes, $this->baseDir);

			if ( is_resource($childProc) )
			{
				stream_set_blocking($childProc, 0);
				stream_set_blocking($childPipes[1], 0);

				fwrite($childPipes[0], serialize($data));
				fclose($childPipes[0]);

				$childData = '';
				
				while ( ! feof($childPipes[1]) )
				{
					$callback();
					$childData .= fgets($childPipes[1], 1024);
				}

				fclose($childPipes[1]);
				fclose($childPipes[2]);
				proc_close($childProc);
				self::debug("closing proc to: ".trim($this->PHP_PATH .' '. $this->baseDir . 'phpitter_get_xml.php'));

				$updates = unserialize($childData);
				self::debug("Got updates: " . count($updates));
			} else {
				return null;
			}

			return $updates;
		}

		function getTwitterXML($url)
		{
			$allUpdates = $this->getAll();
			self::debug("Opening $url");
			$twitterFP = fopen('http://' . $this->username . ':' . $this->password . '@' . $url, 'r');
			if ( ! $twitterFP )
			{
				self::debug("ERROR Opening $url: $twitterFP");
				return array();
			}
			self::debug("Opened $url");

			while ( ! feof($twitterFP) )
			{
				self::debug("Reading $url");
				$friendsTimelineXML .= fgets($twitterFP, 1024);
			}

			fclose($twitterFP);

			$friendsTimelineXML = explode("\n", $friendsTimelineXML);
			$newFriendsTimeline = array();

			self::debug("Parsing $url");
			foreach ( $friendsTimelineXML as $line )
			{
				$line = trim($line);
				preg_match('|<([^>]+)>|', $line, $tag);
				preg_match('|<[^>]+>(.*)</[^>]+>|', $line, $data);
				switch($tag[1])
				{
					case 'status':
						$status = array();
						$user = false;
						break;
					case 'in_reply_to_status_id':
						if ( $data[1] > 0 )
						{
							if ( ! isset($allUpdates[$status['id']]['in_reply_to']))
							{
								self::debug("Fetching ID: " . $data[1]);
								$in_reply_to = $this->getStatusByID($data[1]);
								if ( $in_reply_to != null )
									$status['in_reply_to'] = $in_reply_to['text'];
							} else {
								$status['in_reply_to'] = $allUpdates[$status['id']]['in_reply_to'];
							}
						}
						break;
					case 'created_at':
					case 'id':
					case 'text':
					case 'source':
					case 'truncated':
					case 'in_reply_to':
					case 'in_reply_to_user_id':
					case 'screen_name':
					case 'location':
					case 'description':
					case 'url':
					case 'protected':
					case 'followers_count':
					case 'name':
						if ( ! $user )
							$status[$tag[1]] = $data[1];
						else
							$status['user'][$tag[1]] = $data[1];
						break;
					case 'profile_image_url':
						if ( $data[1] == 'http://static.twitter.com/images/default_profile_normal.png' )
							$status['user']['local_image'] = 'images/default_profile_normal.png';
						else {
							$localImage = preg_replace('|.*/profile_images|', 'images', $data[1]);
							$status['user']['local_image'] = $localImage;
							if ( ! is_readable($localImage) )
							{
								$dir = str_replace(basename($localImage), '', $localImage);
								mkdir($dir, 0755, true);
								$userImage = file_get_contents($data[1]);
								if ( $userImage === false )
									$status['user']['local_image'] = 'images/default_profile_normal.png';
								else
									file_put_contents($localImage, $userImage);
							}
						}
						break;
					case 'user':
						$user = true;
						break;
					case '/user':
						$user = false;
						break;
					case '/status':
						if  ( $status['id'] > 0 )
							$newFriendsTimeline[$status['id']] = $status;
						break;
				}
			}
			self::debug("Done Parsing $url. Returning " . count($newFriendsTimeline));

			return $newFriendsTimeline;
		}

		function getFriendsTimeline($nonBlockingCallback = '')
		{
			if ( $this->offline )
			{
				$this->friendsTimelineLastPull = mktime();
				return;
			}

			if ( $nonBlockingCallback != '' )
			{
				self::debug("calling getXMLNonBlocking");
				$updates = $this->getXMLNonBlocking('friends', $nonBlockingCallback);
				self::debug("Done wtih getXMLNonBlocking");
			} else {
				$updates = $this->getTwitterXML($this->friendsTimelineURL);
			}
			$this->friendsTimelineLastPull = mktime();
			$this->status = date('g:i A', $this->friendsTimelineLastPull) . ' Got Timeline';

			$newUpdates = array_diff_key($updates, $this->friendsTimeline);

			if ( count($newUpdates) > 0 )
			{
				$this->friendsTimeline = $newUpdates + $this->friendsTimeline;
				if ( count($this->friendsTimeline) > $this->maxFriendsUpdatesHistory )
				{
					$timelineChunks = array_chunk($this->friendsTimeline, $this->maxFriendsUpdatesHistory, true);
					$this->friendsTimeline = $timelineChunks[0];
					unset($timelineChunks);
				}

				if ( $nonBlockingCallback != '' )
				{
					self::debug("calling getXMLNonBlocking");
					$updates = $this->getXMLNonBlocking('replies', $nonBlockingCallback);
					self::debug("Done wtih getXMLNonBlocking");
					$this->friendsTimelineLastPull = mktime();
				} else {
					$updates = $this->getTwitterXML($this->repliesURL);
				}

				$newReplies = array_diff_key($updates, $this->replies);
				if ( count($newReplies) > 0 )
				{
					$this->replies = $newReplies + $this->replies;
					if ( count($this->replies) > $this->maxFriendsUpdatesHistory )
					{
						$timelineChunks = array_chunk($this->replies, $this->maxFriendsUpdatesHistory, true);
						$this->replies = $timelineChunks[0];
						unset($timelineChunks);
					}
				}

				$this->justSent = array();
			}

			self::debug("Returning new updates: " . count($newUpdates));
			return $newUpdates;
		}

		function getDirects($nonBlockingCallback = '')
		{
			if ( $this->offline )
			{
				$this->directMessagesLastPull = mktime();
				return;
			}

			if ( $nonBlockingCallback != '' )
			{
				$updates = $this->getXMLNonBlocking('directs', $nonBlockingCallback);
			} else {
				$updates = $this->getDirectMessages($this->directMessagesURL);
			}
			$this->directMessagesLastPull = mktime();
			$this->status = date('g:i A', $this->directMessagesLastPull) . ' Got Directs';

			$newUpdates = array_diff_key($updates, $this->directMessages);

			if ( count($newUpdates) > 0 )
			{
				$this->directMessages = $newUpdates + $this->directMessages;
				if ( count($this->directMessages) > $this->maxFriendsUpdatesHistory )
				{
					$timelineChunks = array_chunk($this->directMessages, $this->maxFriendsUpdatesHistory, true);
					$this->directMessages = $timelineChunks[0];
					unset($timelineChunks);
				}
			}

			if ( $nonBlockingCallback != '' )
			{
				$updates = $this->getXMLNonBlocking('directs_sent', $nonBlockingCallback);
			} else {
				$updates = $this->getDirectMessages($this->directMessagesSentURL);
			}

			$newSent = array_diff_key($updates, $this->directMessagesSent);

			if ( count($newSent) > 0 )
			{
				$this->directMessagesSent = $newSent + $this->directMessagesSent;
				if ( count($this->directMessagesSent) > $this->maxFriendsUpdatesHistory )
				{
					$timelineChunks = array_chunk($this->directMessagesSent, $this->maxFriendsUpdatesHistory, true);
					$this->directMessagesSent = $timelineChunks[0];
					unset($timelineChunks);
				}
			}

			$newUpdates = $newUpdates + $newSent;

			if ( count($newUpdates) > 0 )
				krsort($newUpdates, SORT_NUMERIC);

			return $newUpdates;
		}

		static function sortByID($a, $b)
		{
			return ($a['id'] < $b['id']) ? 1 : -1;
		}

		static function sortByDate($a, $b)
		{
			$date_a = strtotime($a['created_at']);
			$date_b = strtotime($b['created_at']);
			return ($date_a < $date_b) ? -1 : 1;
		}

		function getDirectMessages($url)
		{
			self::debug("Opening $url");
			$twitterFP = fopen('http://' . $this->username . ':' . $this->password . '@' . $url, 'r');
			$this->directMessagesLastPull = mktime();
			if ( ! $twitterFP )
			{
				self::debug("ERROR Opening $url: $twitterFP");
				return array();
			}
			self::debug("Opened $url");

			while ( ! feof($twitterFP) )
			{
				$directMessagesXML .= fgets($twitterFP, 128);
			}

			fclose($twitterFP);

			$directMessagesXML = explode("\n", $directMessagesXML);
			$newDirectMessages = array();

			foreach ( $directMessagesXML as $line )
			{
				$line = trim($line);
				preg_match('|<([^>]+)>|', $line, $tag);
				preg_match('|<[^>]+>(.*)</[^>]+>|', $line, $data);
				switch($tag[1])
				{
					case 'direct_message':
						$message = array();
						$sender = false;
						$recipient = false;
						break;
					case 'sender':
						$sender = true;
						break;
					case '/sender':
						$sender = false;
						break;
					case 'recipient':
						$recipient = true;
						break;
					case '/recipient':
						$recipient = false;
						break;
					case 'id':
					case 'text':
					case 'created_at':
					case 'protected':
					case 'name':
					case 'screen_name':
					case 'description':
					case 'location':
					case 'url':
					case 'followers_count':
						if ( ! $recipient )
						{
							if ( ! $sender )
								$message[$tag[1]] = $data[1];
							else
								$message['user'][$tag[1]] = $data[1];
						} else {
							$message['recipient'][$tag[1]] = $data[1];
						}
						break;
					case 'profile_image_url':
						if ( ! $recipient )
						{
							if ( $data[1] == 'http://static.twitter.com/images/default_profile_normal.png' )
								$message['user']['local_image'] = 'images/default_profile_normal.png';
							else {
								$localImage = preg_replace('|.*/profile_images|', 'images', $data[1]);
								$message['user']['local_image'] = $localImage;
								if ( ! is_readable($localImage) )
								{
									$dir = str_replace(basename($localImage), '', $localImage);
									mkdir($dir, 0755, true);
									file_put_contents($localImage, file_get_contents($data[1]));
								}
							}
						} else {
							if ( $data[1] == 'http://static.twitter.com/images/default_profile_normal.png' )
								$message['recipient']['local_image'] = 'images/default_profile_normal.png';
							else {
								$localImage = preg_replace('|.*/profile_images|', 'images', $data[1]);
								$message['recipient']['local_image'] = $localImage;
								if ( ! is_readable($localImage) )
								{
									$dir = str_replace(basename($localImage), '', $localImage);
									mkdir($dir, 0755, true);
									file_put_contents($localImage, file_get_contents($data[1]));
								}
							}
						}
						break;
					case '/direct_message':
						$message['type'] = 'direct_message';
						$newDirectMessages[$message['id']] = $message;
						break;
					case 'sender_id':
					case 'recipient_id':
					case 'recipient_screen_name':
					case 'sender_screen_name':
						break;
					default:
						break;
				}
			}

			self::debug("Got updates: " . count($newDirectMessages));

			return $newDirectMessages;
		}

		function sendDirect($user, $message)
		{
			$postData['text'] = $message;
			$postData['source'] = $this->userAgent;
			$postData['user'] = $user;

			$postData = http_build_query($postData);
			$opts = array('http' => array( 'method'  => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', 'user_agent' => $this->userAgent, 'content' => $postData));
			$context  = stream_context_create($opts);
			$postURL = 'http://' . $this->username . ':' . $this->password . '@' . $this->sendDirectURL;

			$results = file_get_contents($postURL, false, $context);
		}

		function sendUpdate($message)
		{
			$postData['status'] = $message;
			$postData['source'] = $this->userAgent;
			$postData = http_build_query($postData);
			$opts = array('http' => array( 'method'  => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', 'user_agent' => $this->userAgent, 'content' => $postData));
			$context  = stream_context_create($opts);
			$postURL = 'http://' . $this->username . ':' . $this->password . '@' . $this->sendUpdateURL . '?source=' . $this->userAgent;
			$results = file_get_contents($postURL, false, $context);

			$friendsTimelineXML = explode("\n", $results);
			$newFriendsTimeline = array();

			foreach ( $friendsTimelineXML as $line )
			{
				$line = trim($line);
				preg_match('|<([^>]+)>|', $line, $tag);
				preg_match('|<[^>]+>(.*)</[^>]+>|', $line, $data);
				switch($tag[1])
				{
					case 'status':
						$status = array();
						$user = false;
						break;
					case 'created_at':
					case 'id':
					case 'text':
					case 'source':
					case 'truncated':
					case 'in_reply_to':
					case 'in_reply_to_user_id':
					case 'screen_name':
					case 'location':
					case 'description':
					case 'url':
					case 'protected':
					case 'followers_count':
					case 'name':
						if ( ! $user )
							$status[$tag[1]] = $data[1];
						else
							$status['user'][$tag[1]] = $data[1];
						break;
					case 'profile_image_url':
						if ( $data[1] == 'http://static.twitter.com/images/default_profile_normal.png' )
							$status['user']['local_image'] = 'images/default_profile_normal.png';
						else {
							$localImage = preg_replace('|.*/profile_images|', 'images', $data[1]);
							$status['user']['local_image'] = $localImage;
							if ( ! is_readable($localImage) )
							{
								$dir = str_replace(basename($localImage), '', $localImage);
								mkdir($dir, 0755, true);
								file_put_contents($localImage, file_get_contents($data[1]));
							}
						}
						break;
					case 'user':
						$user = true;
						break;
					case '/user':
						$user = false;
						break;
					case '/status':
						$newFriendsTimeline = $status;
						break;
				}
			}

			if ( $newFriendsTimeline['id'] > 0 )
				$this->justSent[$newFriendsTimeline['id']] = $newFriendsTimeline;
		}

		function sendMapData()
		{
			$postData['username'] = $this->username;
			$postData['data'] = urlencode(gzcompress(serialize($this->friendsTimeline)));
			$postData['urlencoded'] = true;
			$postData = http_build_query($postData);

			$opts = array('http' => array( 'method'  => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', 'user_agent' => $this->userAgent, 'content' => $postData));
			$context  = stream_context_create($opts);
			$postURL = 'http://' . $this->mapDataPostURL;
			$results = file_get_contents($postURL, false, $context);

			return $results;
		}

		function getStatusByID($id)
		{
			$results = file_get_contents('http://' . $this->showUpdateURL . $id . '.xml');

			$friendsTimelineXML = explode("\n", $results);
			$newFriendsTimeline = array();

			foreach ( $friendsTimelineXML as $line )
			{
				$line = trim($line);
				preg_match('|<([^>]+)>|', $line, $tag);
				preg_match('|<[^>]+>(.*)</[^>]+>|', $line, $data);
				switch($tag[1])
				{
					case 'status':
						$status = array();
						$status['id'] = '';
						break;
					case 'id':
						if ( $status['id'] == '' )
							$status['id'] = $data[1];
						break;
					case 'text':
						$status[$tag[1]] = $data[1];
						break;
					case '/status':
						$newFriendsTimeline = $status;
						break;
				}
			}

			if ( $newFriendsTimeline['id'] > 0 )
				return $newFriendsTimeline;

			return null;
		}
	
		public static function debug($msg)
		{
			if (self::DEBUG)
			{
				$log =& self::getLogger('phpTwitter');
				$log->debug($msg);
			}
		}

		public static function getLogger($ident='DEBUG')
		{
			if (self::DEBUG)
			{
				include_once('Log.php');
				if (class_exists('Log'))
				{
					return Log::factory('file','log_phpitter.txt', $ident);
				} else {
					return new DummyLogger();
				}
			} else {
				return new DummyLogger();
			}
		}
	}
	
	/**
         * DummyLogger is a class to take the place of a real PEAR Log instance
         * in the event that Log cannot be instantiated and we don't want to break
         * things ...
         *
         */
        class DummyLogger
        {
                /**
                 * debug does nothing! :)
                 *
                 * @param string $str
                 */
                public function debug($str) {
                }
        }
?>
